package com.retry.task.core.utils;

import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import com.google.common.io.ByteSource;
import com.retry.task.core.exception.ExceptionCode;
import com.retry.task.core.model.base.Result;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

/**
 * @author gao.gwq
 * @version 1.0
 * @date 2022/5/7  17:07
 * @Description TODO
 */
public class HttpClientUtils {
    private static Logger LOG = LoggerFactory.getLogger(HttpClientUtils.class);

    public static final String TRACE_ID = "traceId";

    private static PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager();
    private static RequestConfig requestConfig;

    private static HttpClientBuilder httpClientBuilder = null;
    private static CloseableHttpClient httpClient = null;

    private HttpClientUtils() {
    }

    static {
        httpClientBuilder = HttpClientBuilder.create();
        connMgr.setMaxTotal(512);
        connMgr.setDefaultMaxPerRoute(100);
        RequestConfig.Builder configBuilder = RequestConfig.custom();
        configBuilder.setConnectTimeout(100000);
        configBuilder.setSocketTimeout(400000);
        configBuilder.setConnectionRequestTimeout(7000);
        configBuilder.setStaleConnectionCheckEnabled(true);
        requestConfig = configBuilder.build();
        httpClientBuilder.setConnectionManager(connMgr);
        httpClientBuilder.setDefaultRequestConfig(requestConfig);
        httpClient = httpClientBuilder.build();
    }

    public static abstract class HttpClientOptions {

        public Map<String, Object> getHeaders() {
            return new HashMap<String, Object>();
        }

        public int getConnectTimeout() {
            return 500;
        }
    }

    public static String get(String url) {
        return get(url, new HashMap<>(), new HttpClientOptions() {
        });
    }

    public static String get(String url, Map<String, Object> params, HttpClientOptions options) {
        return getResponse(url, params, options)
            .filter((resp) -> resp.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
            .map(HttpClientUtils::responseToString)
            .orElse(null);
    }

    public static String responseToString(HttpResponse response) {
        HttpEntity entity = response.getEntity();
        if (entity == null) {
            return null;
        }
        try {
            InputStream inputStream = entity.getContent();
            ByteSource byteSource = new ByteSource() {
                @Override
                public InputStream openStream() {
                    return inputStream;
                }
            };
            return byteSource.asCharSource(Charsets.UTF_8).read();
        } catch (IOException e) {
            return null;
        }
    }

    public static Optional<HttpResponse> getResponse(String url, Map<String, Object> params,
        HttpClientOptions options) {
        URIBuilder uriBuilder;
        try {
            uriBuilder = new URIBuilder(url);
            params.forEach((key, value) -> uriBuilder.addParameter(key, value.toString()));
        } catch (URISyntaxException e) {
            LOG.error("URISyntaxException {}", e.getMessage(), e);
            return Optional.empty();
        }
        HttpGet request = new HttpGet(uriBuilder.toString());
        request.setConfig(
            RequestConfig.custom()
                .setConnectTimeout(options.getConnectTimeout())
                .build()
        );
        Map<String, Object> headerMap = options.getHeaders();
        headerMap.forEach((String name, Object value) -> {
            request.setHeader(name, value.toString());
        });
        CloseableHttpResponse response;
        try {
            response = httpClient.execute(request);
        } catch (IOException e) {
            return Optional.empty();
        }
        return Optional.of(response);
    }

    public static <R> R doGet(String url, Function<InputStream, R> function) throws Exception {
        return doGet(url, new HashMap<>(), null, function);
    }

    public static <R> R doGet(String url, Map<String, String> uriParams, Consumer<HttpGet> httpGetConsumer,
        Function<InputStream, R> function) throws Exception {
        R result = null;
        if (StringUtils.isEmpty(url)) {
            LOG.info("warn:doGet url is null or '' ");
            return result;
        }
        URIBuilder URIBuilder = generateURIBuilder(url, uriParams);
        CloseableHttpResponse response = null;
        InputStream instream = null;
        URI uri = URIBuilder.build();
        HttpGet httpGet = new HttpGet(uri);
        try {

            if (httpGetConsumer != null) {
                httpGetConsumer.accept(httpGet);
            }
            response = httpClient.execute(httpGet);
            int statusCode = response.getStatusLine().getStatusCode();
            LOG.info("doGet statusCode:{}", statusCode);
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                instream = entity.getContent();
                result = function.apply(instream);
            }
        } catch (IOException ex) {
            LOG.error("doGet  IO ERROR :{}", ex.getMessage(), ex);
            throw new IOException(ex);
        } catch (Exception ex) {
            LOG.error("doGet URISyntaxException :{}", ex.getMessage(), ex);
            throw new Exception(ex);
        } finally {
            httpGet.releaseConnection();
            IOUtils.closeQuietly(instream);
            IOUtils.closeQuietly(response);

        }
        return result;
    }

    public static URIBuilder generateURIBuilder(String url, Map<String, String> params) throws URISyntaxException {
        URIBuilder URIBuilder = new URIBuilder(url);
        if (!CollectionUtils.isEmpty(params)) {
            List<NameValuePair> pairList = new ArrayList(params.size());
            Iterator paramIter = params.entrySet().iterator();

            while (paramIter.hasNext()) {
                Map.Entry entry = (Map.Entry)paramIter.next();
                NameValuePair pair = new BasicNameValuePair((String)entry.getKey(), entry.getValue().toString());
                pairList.add(pair);
            }
            URIBuilder.addParameters(pairList);
        }
        return URIBuilder;
    }

    public static String doPost(String apiUrl, Function<InputStream, String> function) throws Exception {
        return doPost(apiUrl, new HashMap<String, String>(), new HashMap<String, String>(), function);
    }

    public static <R> R doPost(String apiUrl, Map<String, String> params,
        Map<String, String> headers,
        Function<InputStream, R> function) throws Exception {
        R result = null;
        if (StringUtils.isEmpty(apiUrl)) {
            LOG.info("warn:doPost url is null or '' ");
            return result;
        }
        HttpPost httpPost = new HttpPost(apiUrl);
        String param = generateURLParams(params, httpPost);
        LOG.info("http请求地址:" + apiUrl + "?" + param);

        if (!CollectionUtils.isEmpty(headers)) {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                httpPost.addHeader(entry.getKey(), entry.getValue());
            }
        }
        CloseableHttpResponse response = null;
        InputStream instream = null;
        try {
            response = httpClient.execute(httpPost);
            int statusCode = response.getStatusLine().getStatusCode();
            LOG.info("doPost statusCode:{}", statusCode);
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                instream = entity.getContent();
                return function.apply(instream);
            }
        } catch (Exception ex) {
            LOG.error("doPost  ERROR :{}", ex.getMessage(), ex);
            throw new Exception(ex);
        } finally {
            IOUtils.closeQuietly(instream);
            IOUtils.closeQuietly(response);
            httpPost.releaseConnection();
        }
        return result;
    }

    private static String generateURLParams(Map<String, String> params, HttpPost httpPost) {
        String param = "";
        if (!CollectionUtils.isEmpty(params)) {
            List<NameValuePair> pairList = new ArrayList(params.size());
            Iterator temParam = params.entrySet().iterator();
            while (temParam.hasNext()) {
                Map.Entry entry = (Map.Entry)temParam.next();
                NameValuePair pair = new BasicNameValuePair((String)entry.getKey(), entry.getValue().toString());
                pairList.add(pair);
                if (param.equals("")) {
                    param = (String)entry.getKey() + "=" + entry.getValue();
                } else {
                    param = param + "&" + (String)entry.getKey() + "=" + entry.getValue();
                }
            }
            httpPost.setEntity(new UrlEncodedFormEntity(pairList, Charset.forName("UTF-8")));
        }
        return param;
    }

    public static Result postBody(String url, Object requestObj,
        Class returnTargClassOfT) {
        if (StringUtils.isEmpty(url)) {
            LOG.info("warn:doPostByJson url is null or '' ");
            return Result.getFail("100", "url is null");
        }
        return doPostNew(url, httpPost -> {
            StringEntity stringEntity = new StringEntity(GsonTool.toJsonStringIgnoreNull(requestObj), "UTF-8");
            httpPost.setHeader("Content-Type", "application/json;charset=utf-8");
            httpPost.setHeader("Accept", "application/json");
            stringEntity.setContentEncoding("UTF-8");
            stringEntity.setContentType("application/json");
            httpPost.setEntity(stringEntity);
        }, httpResponse -> {
            InputStream inputStream = null;
            try {
                inputStream = httpResponse.getEntity().getContent();
                String result = IOUtils.toString(inputStream, "UTF-8");
                Result returnT = GsonTool.fromJson(result, Result.class, returnTargClassOfT);
                return returnT;
            } catch (Exception ex) {
                LOG.error("retry-task get response error ,{}", ex.getMessage(), ex);
                return Result.getFail("100", ex.getMessage());
            }
        });
    }

    public static Result postBody(String url, Object requestObj,
        Class returnTargClassOfT, String traceId) {
        if (StringUtils.isEmpty(url)) {
            LOG.info("warn:doPostByJson url is null or '' ");
            return Result.getFail("100", "url is null");
        }
        return doPostNew(url, httpPost -> {
            StringEntity stringEntity = new StringEntity(GsonTool.toJsonStringIgnoreNull(requestObj), "UTF-8");
            httpPost.setHeader("Content-Type", "application/json;charset=utf-8");
            httpPost.setHeader("Accept", "application/json");
            stringEntity.setContentEncoding("UTF-8");
            stringEntity.setContentType("application/json");
            httpPost.setHeader("traceId", traceId);
            httpPost.setEntity(stringEntity);
        }, httpResponse -> {
            InputStream inputStream = null;
            try {
                inputStream = httpResponse.getEntity().getContent();
                String result = IOUtils.toString(inputStream, "UTF-8");
                LOG.info("result is {}", result);
                Result returnT = GsonTool.fromJson(result, Result.class, returnTargClassOfT);
                return returnT;
            } catch (Exception ex) {
                LOG.error("retry-task get response error ,{}", ex.getMessage(), ex);
                return Result.getFail("100", ex.getMessage());
            }
        });
    }

    public static Result doPostNew(String url,
        Consumer<HttpPost> httpPostConsumer,
        Function<HttpResponse, Result> function) {
        Result result = null;

        boolean useHttps = url.startsWith("https");
        CloseableHttpClient innerHttpClient = httpClient;
        if (useHttps) {
            innerHttpClient = httpClientBuilder.setSSLSocketFactory(createSSLConnSocketFactory())
                .build();
        }
        HttpPost httpPost = new HttpPost(url);
        CloseableHttpResponse response = null;
        InputStream instream = null;
        try {
            httpPostConsumer.accept(httpPost);
            response = innerHttpClient.execute(httpPost);
            int statusCode = response.getStatusLine().getStatusCode();
            LOG.info("retry-task doPost statusCode:{},url is {}", statusCode, url);
            result = function.apply(response);
        } catch (Exception ex) {
            LOG.error("doPost BY JSON ERROR :{},url:{},data:{}", ex.getMessage(), url, ex);
            if (ex instanceof SocketTimeoutException) {
                return Result.getFail(ExceptionCode.SOCKET_TIME_OUT_EXCEPTION,
                    "socket time out exception" + ex.getMessage());
            }
            if (ex instanceof ConnectException) {
                return Result.getFail(ExceptionCode.CONNECTION_EXCEPTION, "connection exception" + ex.getMessage());
            }
            return Result.getFail(Result.FAIL_CODE,
                "retry-task http remote error(" + ex.getMessage() + "), for url : " + url);
        } finally {
            IOUtils.closeQuietly(instream);
            IOUtils.closeQuietly(response);
            httpPost.releaseConnection();
        }
        return result;
    }

    private static final TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() {
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[] {};
        }

        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }
    }};

    private static SSLConnectionSocketFactory createSSLConnSocketFactory() {
        SSLConnectionSocketFactory sslsf = null;

        try {
            /*SSLContext sslContext = (new SSLContextBuilder()).loadTrustMaterial((KeyStore)null, new TrustStrategy() {
                @Override
                public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    return true;
                }
            }).build();*/
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
            sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {
                @Override
                public boolean verify(String arg0, SSLSession arg1) {
                    return true;
                }

                @Override
                public void verify(String host, SSLSocket ssl) throws IOException {
                }

                @Override
                public void verify(String host, X509Certificate cert) throws SSLException {
                }

                @Override
                public void verify(String host, String[] cns, String[] subjectAlts) throws SSLException {
                }
            });
        } catch (Exception ex) {
            LOG.error("createSSLConnSocketFactory ERROR :{}", ex.getMessage(), ex);
        }

        return sslsf;
    }

    public static String doPostPay(String url, Object json, String authorization) throws Exception {
        String result = null;
        if (StringUtils.isEmpty(url)) {
            LOG.info("warn:doPostByJson url is null or '' ");
            return result;
        }
        HttpPost httpPost = new HttpPost(url);
        CloseableHttpResponse response = null;
        InputStream instream = null;

        try {
            setHttpParams(json, authorization, httpPost, "Content-Type", "application/json;charset=utf-8", "Accept",
                "application/json");
            response = httpClient.execute(httpPost);
            int statusCode = response.getStatusLine().getStatusCode();
            LOG.info("doPost statusCode:{}", statusCode);
            HttpEntity entity = response.getEntity();
            if (entity != null) {
                instream = entity.getContent();
                result = IOUtils.toString(instream, "UTF-8");
            }
        } catch (IOException ex) {
            LOG.error("doPost BY JSON ERROR :{}", ex.getMessage(), ex);
        } finally {
            IOUtils.closeQuietly(instream);
            IOUtils.closeQuietly(response);
            httpPost.releaseConnection();
        }

        return result;
    }

    private static void setHttpParams(Object json, String authorization, HttpPost httpPost, String s, String s2,
        String accept, String s3) {
        StringEntity stringEntity = new StringEntity(json.toString(), "UTF-8");
        httpPost.setHeader(s, s2);
        httpPost.setHeader(accept, s3);
        httpPost.setHeader("Authorization", authorization);
        stringEntity.setContentEncoding("UTF-8");
        stringEntity.setContentType("application/json");
        httpPost.setEntity(stringEntity);
    }

}
